Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: toss/frontend-fundamentals
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: f6ca50b2c3
Choose a base ref
...
head repository: toss/frontend-fundamentals
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 049405334a
Choose a head ref
  • 1 commit
  • 23 files changed
  • 1 contributor

Commits on Jan 18, 2025

  1. docs: translate into Japanese (#118)

    * chore: create ja folder and copy files
    
    * docs: translate into japanese
    
    * docs: translate into japanese
    
    * docs: translate for japanese
    
    * docs: reflect #78
    
    * docs: reflect #93
    
    * docs: reflect #97
    
    * docs: update use-user.md (#101)
    
    * docs: reflect #102
    
    * docs: reflect #116
    
    * docs: fix disabled prop code
    kaehehehe authored Jan 18, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    0494053 View commit details
44 changes: 23 additions & 21 deletions .vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,44 +1,46 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import { defineConfig } from 'vitepress';
import footnote from 'markdown-it-footnote';
import { shared } from './shared.mts';
import { en } from './en.mts';
import { ko } from './ko.mts';
import { createRequire } from "node:module";
import path from "node:path";
import { defineConfig } from "vitepress";
import footnote from "markdown-it-footnote";
import { shared } from "./shared.mts";
import { en } from "./en.mts";
import { ko } from "./ko.mts";
import { ja } from "./ja.mts";

const require = createRequire(import.meta.url);

export default defineConfig({
...shared,
locales: {
en: { label: 'English', ...en },
root: { label: '한국어', ...ko },
en: { label: "English", ...en },
ja: { label: "日本語", ...ja },
root: { label: "한국어", ...ko }
},
vite: {
resolve: {
alias: [
{
find: /^vue$/,
replacement: path.dirname(
require.resolve('vue/package.json', {
paths: [require.resolve('vitepress')],
require.resolve("vue/package.json", {
paths: [require.resolve("vitepress")]
})
),
)
},
{
find: /^vue\/server-renderer$/g,
replacement: path.dirname(
require.resolve('vue/server-renderer', {
paths: [require.resolve('vitepress')],
require.resolve("vue/server-renderer", {
paths: [require.resolve("vitepress")]
})
),
},
],
},
)
}
]
}
},
markdown: {
config: (md) => {
md.use(footnote);
},
},
})
}
}
});
239 changes: 239 additions & 0 deletions .vitepress/ja.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { type DefaultTheme, defineConfig } from "vitepress";

export const ja = defineConfig({
lang: "ja",
title: "Frontend Fundamentals",
description: "変更しやすいフロントエンドコードのためのガイドライン",
lastUpdated: true,
head: [
["link", { rel: "icon", href: "/images/favicon.ico" }],
[
"meta",
{
property: "og:image",
content: "https://static.toss.im/illusts/ff-meta.png"
}
],
[
"meta",
{
name: "twitter:image",
content: "https://static.toss.im/illusts/ff-meta.png"
}
],
[
"meta",
{
name: "twitter:card",
content: "https://static.toss.im/illusts/ff-meta.png"
}
]
],
themeConfig: {
logo: "/images/ff-symbol.svg",
nav: nav(),

editLink: {
pattern: "https://github.com/toss/frontend-fundamentals/edit/main/:path",
text: "GitHubで修正する"
},

outline: {
label: "ページの内容"
},
docFooter: {
prev: "前のページ",
next: "次のページ"
},
lastUpdated: {
text: "最後のアップデート"
},

socialLinks: [
{
icon: "github",
link: "https://github.com/toss/frontend-fundamentals"
}
],

sidebar: sidebar()
}
});

function nav(): DefaultTheme.NavItem[] {
return [{ text: "ホーム", link: "/ja" }];
}

function sidebar(): DefaultTheme.Sidebar {
return [
{
text: "良いコードの基準",
items: [
{
text: "はじめる",
link: "/ja/code/start"
},
{
text: "変更しやすいコード",
link: "/ja/code/"
},
{
text: "コミュニティ",
items: [
{
text: "紹介",
link: "/code/community"
},
{
text: "⭐ 良い討論をまとめて見る",
link: "https://github.com/toss/frontend-fundamentals/discussions?discussions_q=is%3Aopen+label%3A%22성지+⛲%22"
},
{
text: "A vs B",
link: "https://github.com/toss/frontend-fundamentals/discussions/categories/a-vs-b?discussions_q=is%3Aopen+category%3A%22A+vs+B%22+sort%3Adate_created"
},
{
text: "自由におしゃべり",
link: "https://github.com/toss/frontend-fundamentals/discussions/categories/open-forum?discussions_q=is%3Aopen+sort%3Adate_created+category%3A%22Open+Forum%22"
}
],
collapsed: true
}
]
},
{
text: "良いコードを書くための戦略",
items: [
{
text: "1. 可読性",
items: [
{
text: "コンテキストを減らす",
items: [
{
text: "A. 一緒に実行されないコードを分離する",
link: "/ja/code/examples/submit-button"
},
{
text: "B. 実装の詳細を抽象化する",
link: "/ja/code/examples/login-start-page"
},
{
text: "C. ロジックの種類に応じて一体化している関数を分ける",
link: "/ja/code/examples/use-page-state-readability"
}
],
collapsed: true
},
{
text: "名前付け",
items: [
{
text: "A. 複雑な条件に名前を付ける",
link: "/ja/code/examples/condition-name"
},
{
text: "B. マジックナンバーに名前を付ける",
link: "/ja/code/examples/magic-number-readability"
}
],
collapsed: true
},
{
text: "上から下へ読めるようにする",
items: [
{
text: "A. 視点の移動を減らす",
link: "/ja/code/examples/user-policy"
},
{
text: "B. 三項演算子をシンプルにする",
link: "/ja/code/examples/ternary-operator"
}
],
collapsed: true
}
]
},

{
text: "2. 予測可能性",
items: [
{
text: "A. 名前が被らないように管理する",
link: "/ja/code/examples/http"
},
{
text: "B. 同じ種類の関数は返り値の型を統一する",
link: "/ja/code/examples/use-user"
},
{
text: "C. 隠れたロジックを露呈させる",
link: "/ja/code/examples/hidden-logic"
}
]
},
{
text: "3. 凝集度",
items: [
{
text: "A. 緒に修正されるファイルは同じディレクトリに置く",
link: "/ja/code/examples/code-directory"
},
{
text: "B. マジックナンバーを排除する",
link: "/ja/code/examples/magic-number-cohesion"
},
{
text: "C. フォームの凝集度について考える",
link: "/ja/code/examples/form-fields"
}
]
},
{
text: "4. 結合度",
items: [
{
text: "A. 責任を一つずつ管理する",
link: "/ja/code/examples/use-page-state-coupling"
},
{
text: "B. 重複コードを許容する",
link: "/ja/code/examples/use-bottom-sheet"
},
{
text: "C. Props Drilling を解消する",
link: "/ja/code/examples/item-edit-modal"
}
]
}
]
}
];
}

export const search: DefaultTheme.LocalSearchOptions["locales"] = {
root: {
translations: {
button: {
buttonText: "検索",
buttonAriaLabel: "検索"
},
modal: {
backButtonTitle: "戻る",
displayDetails: "もっとみる",
footer: {
closeKeyAriaLabel: "閉じる",
closeText: "閉じる",
navigateDownKeyAriaLabel: "下へ",
navigateText: "移動",
navigateUpKeyAriaLabel: "上へ",
selectKeyAriaLabel: "選択",
selectText: "選択"
},
noResultsText: "検索結果がありませんでした。",
resetButtonTitle: "すべて消去"
}
}
}
};
29 changes: 29 additions & 0 deletions ja/code/community.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
comments: false
---

# コミュニティ

`Frontend Fundamentals`(FF)は、コミュニティと共に良いコードの基準を作り上げています。

現在はTossのフロントエンドチャプターが運営をしています。

## 良い議論をまとめて見る

コミュニティで行われた良い議論を見てみましょう。Frontend Fundamentalsのドキュメントに掲載されている内容を超えて、良いコードについての考えを広げることができます。

- [良い議論をまとめて見る](https://github.com/toss/frontend-fundamentals/discussions?discussions_q=is%3Aopen+label%3A%22성지+⛲%22)

## 悩んでいるコードについて議論する

悩んでいるコードがある場合は、GitHubディスカッションに投稿してみましょう。自分のコードについてコミュニティから様々なレビューを受けることができ、良いコードの基準についてコミュニティと共に考えることができます。また、多くの共感を得た事例は、直接Frontend Fundamentalsの文書に投稿することができます。貢献方法は後日公開される予定です。

- [Githubディスカッションに投稿する](https://github.com/toss/frontend-fundamentals/discussions)

## 良いコードの基準に意見を追加する

良いコードの基準について意見がある場合や、新しい意見を追加したい場合は、どのコードがより良いか投票し、意見を残してください。コミュニティと交流しながら、より豊かで深い基準を作り上げていきましょう。

このコードは良いのか?あのコードは良いのか?ということについて、自分自身の基準を確立するきっかけになるかもしれません。

- [A vs Bに投稿されたコードを見る](https://github.com/toss/frontend-fundamentals/discussions/categories/a-vs-b)
77 changes: 77 additions & 0 deletions ja/code/examples/code-directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 一緒に修正されるファイルは同じディレクトリに置く

<div style="margin-top: 16px">
<Badge type="info" text="凝集度" />
</div>

プロジェクトでコードを作成していると、Hook、コンポーネント、ユーティリティ関数などを複数のファイルに分けて管理することになります。こうしたファイルを簡単に作成、検索、削除できるように、適切なディレクトリ構造を確立することが重要です。

関連するソースファイルを同じディレクトリに配置することで、コードの依存関係を明確に示すことができます。これにより、参照してはいけないファイルを誤って参照するのを防ぎ、関連するファイルを一度に削除することも可能になります。

## 📝 コード例

次のコードは、プロジェクトのすべてのファイルをモジュールの種類(Presentational コンポーネント、Container コンポーネント、Hook、定数など)に応じて分類したディレクトリ構造です。

```text
└─ src
├─ components
├─ constants
├─ containers
├─ contexts
├─ remotes
├─ hooks
├─ utils
└─ ...
```

## 👃 コードの不吉な臭いを嗅いでみる

### 凝集度

ファイルをこのように種類別に分けると、どのコードがどのコードを参照しているのかを簡単に確認できなくなります。コードファイル間の依存関係を開発者が自らコードを分析しながら把握する必要があります。
また、特定のコンポーネントや Hook、ユーティリティ関数が不要になり削除する場合、関連するコードが一緒に削除されずに残ってしまうこともあります。

プロジェクトの規模は時間とともに大きくなることが一般的で、プロジェクトのサイズが 2 倍、10 倍、100 倍と大きくなるにつれて、コード間の依存関係も同時に複雑になる可能性があります。1 つのディレクトリに 100 を超えるファイルが含まれることもあるでしょう。

## ✏️ リファクタリングしてみる

次のコードは、関連して修正されるコードファイルを一つのディレクトリにまとめるように構造を改善した例です。

```text
└─ src
│ // プロジェクト全体で使われるコード
├─ components
├─ containers
├─ hooks
├─ utils
├─ ...
└─ domains
│ // Domain1でのみ使われるコード
├─ Domain1
│ ├─ components
│ ├─ containers
│ ├─ hooks
│ ├─ utils
│ └─ ...
│ // Domain2でのみ使われるコード
└─ Domain2
├─ components
├─ containers
├─ hooks
├─ utils
└─ ...
```

一緒に修正されるコードファイルを一つのディレクトリに配置すれば、コード間の依存関係を把握しやすくなります。

例えば、次のようにあるドメイン(`Domain1`)の下にあるコードで別のドメイン(`Domain2`)のソースコードを参照していると考えてみましょう。

```typescript
import { useFoo } '../../../Domain2/hooks/useFoo'
```
このような import 文に遭遇した場合、誤ったファイルを参照していることを容易に認識できるようになります。
また、特定の機能に関連するコードを削除する際に、1 つのディレクトリ全体を削除すれば、すべてのコードがきれいに削除されるため、プロジェクト内に不要なコードが残らないようにすることができます。
68 changes: 68 additions & 0 deletions ja/code/examples/condition-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 複雑な条件に名前を付ける

<div style="margin-top: 16px">
<Badge type="info" text="可読性" />
</div>

複雑な条件式が何の名前もなく使用されると、その条件が意味することを一目で把握するのが難しくなります。

## 📝 コード例

次のコードは、商品の中でカテゴリーと価格範囲が一致する商品だけをフィルタリングするロジックです。

```typescript
const result = products.filter((product) =>
product.categories.some(
(category) =>
category.id === targetCategory.id &&
product.prices.some((price) => price >= minPrice && price <= maxPrice)
)
);
```

## 👃 コードの不吉な臭いを嗅いでみる

### 可読性

このコードでは、匿名関数と条件が複雑に絡み合っています。`filter``some``&&`などのロジックが何重にもなっているため、正確な条件を把握するのが難しくなっています。

コードを読む人が一度に考慮しなければならないコンテキストが多いため、可読性が低下しています。[^1]

[^1]: [プログラマー脳](https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC%E8%84%B3-%EF%BD%9E%E5%84%AA%E3%82%8C%E3%81%9F%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E8%AA%8D%E7%9F%A5%E7%A7%91%E5%AD%A6%E3%81%AB%E5%9F%BA%E3%81%A5%E3%81%8F%E3%82%A2%E3%83%97%E3%83%AD%E3%83%BC%E3%83%81-Felienne-Hermans/dp/4798068535)によると、人間の脳が一度に保存できる情報の数は 6 個だそうです。

### ✏️ リファクタリングしてみる

次のコードのように条件に明示的な名前を付けると、コードを読む人が一度に考慮しなければならないコンテキストを減らすことができます。

```typescript
const matchedProducts = products.filter((product) => {
return product.categories.some((category) => {
const isSameCategory = category.id === targetCategory.id;
const isPriceInRange = product.prices.some(
(price) => price >= minPrice && price <= maxPrice
);

return isSameCategory && isPriceInRange;
});
});
```

明示的に同じカテゴリーに属し、価格範囲が一致する商品でフィルタリングするように書くことで、複雑な条件式を追わなくてもコードの意図を明確に示すことができます。

## 🔍 さらに詳しく: 条件式に名前を付ける基準

いつ条件式や関数に名前を付けて分離するのが良いのでしょうか?

### 条件に名前を付けるべき時

- **複雑なロジックを扱う時**: 条件文や関数で複雑なロジックが複数行にわたって処理される場合、名前を付けて関数の役割を明確にしたほうが良いです。こうすることによってコードの可読性が向上し、保守やコードレビューが容易になります。

- **再利用性が必要な時**: 同じロジックを複数の場所で繰り返し使用する可能性がある場合、変数や関数を宣言して再利用できます。これによりコードの重複を減らし、保守が容易になります。

- **単体テストが必要な時**: 関数を分離すると、独立して単体テストを作成できます。単体テストは関数が正しく動作しているかを簡単に確認でき、特に複雑なロジックをテストする際に有効です。

### 条件に名前を付けなくても良い時

- **ロジックが簡単な時**: ロジックが非常に簡単な場合は、わざわざ名前を付ける必要はありません。例えば、配列の要素を単に 2 倍にする `arr.map(x => x * 2)`のようなコードは、名前を付けなくても直観的です。

- **一度だけしか使用しない時**: 特定のロジックがコード内で一度だけ使用され、そのロジックが複雑でない場合、匿名関数で直接ロジックを処理する方が直観的かもしれません。
Empty file.
154 changes: 154 additions & 0 deletions ja/code/examples/form-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# フォームの凝集度について考える
<div style="margin-top: 16px">
<Badge type="info" text="凝集度" />
</div>

フロントエンド開発をしていると、ユーザーから値を入力してもらうためにフォームを使用することがよくあります。
フォームを管理する際には、2つの方法で凝集度を保ち、一緒に修正すべきコードが同時に修正されるようにすることができます。

## フィールド単位の凝集度

フィールド単位の凝集度は、個別の入力要素を独立して管理する方法です。
各フィールドが独自の検証ロジックを持つため、変更が必要な範囲が縮小され、特定のフィールドのメンテナンスが容易になります。
フィールド単位の凝集度を考慮して設計すると、各フィールドの検証ロジックが独立しているため、他のフィールドに影響を与えることがありません。

```tsx
import { useForm } from "react-hook-form";

export function Form() {
const {
register,
formState: { errors },
handleSubmit
} = useForm({
defaultValues: {
name: "",
email: ""
}
});

const onSubmit = handleSubmit((formData) => {
// フォームデータを送信するロジック
console.log("Form submitted:", formData);
});

return (
<form onSubmit={onSubmit}>
<div>
<input
{...register("name", {
validate: (value) =>
isEmptyStringOrNil(value) ? "名前を入力してください。" : ""
})}
placeholder="名前"
/>
{errors.name && <p>{errors.name.message}</p>}
</div>

<div>
<input
{...register("email", {
validate: (value) => {
if (isEmptyStringOrNil(value)) {
return "メールアドレスを入力してください。";
}

if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) {
return "正しいメールアドレスを入力してください。";
}

return "";
}
})}
placeholder="メールアドレス"
/>
{errors.email && <p>{errors.email.message}</p>}
</div>

<button type="submit">送信</button>
</form>
);
}

function isNil(value: unknown): value is null | undefined {
return value == null;
}

type NullableString = string | null | undefined;

function isEmptyStringOrNil(value: NullableString): boolean {
return isNil(value) || value.trim() === "";
}
```

## フォーム全体の凝集度

フォーム全体の凝集度とは、すべてのフィールドの検証ロジックがフォームに依存する設計のことです。この設計は、フォーム全体の流れを考慮して行われ、変更がフォーム単位で発生する場合に特に重要です。

フォーム全体の凝集度を高めると、検証が一箇所で管理されるため、ロジックがシンプルになり、状態も中央で管理できるようになります。これにより、フォーム全体の流れを把握しやすくなります。しかし、フィールド間の結合度が高まるため、フォームの再利用性が下がる可能性もあります。

```tsx
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const schema = z.object({
name: z.string().min(1, "名前を入力してください。"),
email: z
.string()
.min(1, "メールアドレスを入力してください。")
.email("正しいメールアドレスを入力してください。")
});

export function Form() {
const {
register,
formState: { errors },
handleSubmit
} = useForm({
defaultValues: {
name: "",
email: ""
},
resolver: zodResolver(schema)
});

const onSubmit = handleSubmit((formData) => {
// フォームデータを送信するロジック
console.log("Form submitted:", formData);
});

return (
<form onSubmit={onSubmit}>
<div>
<input {...register("name")} placeholder="名前" />
{errors.name && <p>{errors.name.message}</p>}
</div>

<div>
<input {...register("email")} placeholder="メールアドレス" />
{errors.email && <p>{errors.email.message}</p>}
</div>

<button type="submit">送信</button>
</form>
);
}
```

## フィールド単位 vs. フォーム全体の凝集度

凝集度を高めるためには、フィールド単位とフォーム全体単位のどちらが状況に適しているかを選ぶことが大切です。
フィールド単位で分けると再利用性と独立性が高まりますが、フォーム全体単位で管理することで一貫した流れを保つことができます。

変更単位がフィールド単位かフォーム全体単位かによって、設計を調整する必要があります。

### フィールド単位の凝集度を選ぶべき時

- **独立した検証が必要な場合**: フィールドごとに複雑な検証ロジックや非同期検証が必要な場合に適しています。メールアドレスの形式チェックや電話番号の有効性確認、IDの重複チェック、推薦コードの有効性確認など、各フィールドが独立してそれぞれの検証を行う必要があるときに便利です。
- **再利用が必要な場合**: フィールドと検証ロジックが他のフォームでも同様に使用できる場合です。共通の入力フィールドを独立して管理し、再利用したいときに便利です。
### フォーム全体単位の凝集度を選ぶべき時

- **単一機能を示す場合**: すべてのフィールドが密接に関連して、ひとつの完結した機能を構成する時に有用です。決済情報や配送情報のように、すべてのフィールドが一つのビジネスロジックを形成する場合に適しています。
- **段階的な入力が必要な場合**: ウィザードフォームのように、ステップごとに動作する複雑なフォームに適しています。会員登録やアンケートのように、前のステップの入力が次のステップに影響を与える場合に適しています。
- **フィールド間の依存性がある場合**: 複数のフィールドが互いに参照したり影響を与えたりする場合に適しています。パスワード確認や合計計算のように、フィールド間の相互作用が必要な場合に便利です。
56 changes: 56 additions & 0 deletions ja/code/examples/hidden-logic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 隠れたロジックを露呈させる

<div style="margin-top: 16px">
<Badge type="info" text="予測可能性" />
</div>

関数やコンポーネントの名前、パラメータ、返り値に明示されていない隠れたロジックがある場合、一緒に開発をしているチームメンバーがその挙動を予測するのが困難になる可能性があります。

## 📝 コード例

次のコードは、ユーザーの口座残高を照会する際に使用できる`fetchBalance`関数です。この関数を呼び出すたびに、暗黙的に `balance_fetched`というロギングが行われています。

```typescript 4
async function fetchBalance(): Promise<number> {
const balance = await http.get<number>("...");

logging.log("balance_fetched");

return balance;
}
```

## 👃 コードの不吉な臭いを嗅いでみる

### 予測可能性

`fetchBalance`関数の名前と返り値の型だけでは、`balance_fetched`というロギングが行われるかどうか分かりません。そのため、ロギングを望まない場所でもロギングが行われてしまう可能性があります。

また、ロギングのロジックにエラーが発生した場合、突然口座残高を取得するロジックが壊れてしまう可能性もあるでしょう。

## ✏️ リファクタリングしてみる

関数の名前、パラメータ、返り値の型から予測できるロジックだけを実装部分に残してください。

```typescript
async function fetchBalance(): Promise<number> {
const balance = await http.get<number>("...");

return balance;
}
```

そして、ロギングを行うコードは別に分離してください。

```tsx
<Button
onClick={async () => {
const balance = await fetchBalance();
logging.log("balance_fetched");

await syncBalance(balance);
}}
>
口座残高を更新する
</Button>
```
86 changes: 86 additions & 0 deletions ja/code/examples/http.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 名前が被らないように管理する

<div style="margin-top: 16px">
<Badge type="info" text="予測可能性" />
</div>

同じ名前を持つ関数や変数は、同じ挙動をするべきです。小さな挙動の違いはコードの予測可能性を低下させ、コードを読む人に誤解を招く可能性があります。

## 📝  コード例

とあるフロントエンドサービスで、元々使用していたHTTPライブラリをラップして新しい形でHTTPリクエストを送るモジュールを作成しました。
偶然にも、元のHTTPライブラリと新しく作成したHTTPモジュールの名前は`http`という同じ名前です。

::: code-group

```typescript [http.ts]
// このサービスは`http`というライブラリを使っています
import { http as httpLibrary } from "@some-library/http";

export const http = {
async get(url: string) {
const token = await fetchToken();

return httpLibrary.get(url);
}
};
```

```typescript [fetchUser.ts]
// http.tsで定義したhttpを持ってくるコード
import { http } from "./http";

export async function fetchUser() {
return http.get("...");
}
```

:::

## 👃 コードの不吉な臭いを嗅いでみる

### 予測可能性

このコードは機能的には問題ありませんが、読む人に誤解を与える可能性があります。`http.get`を呼び出す開発者は、この関数が元のHTTPライブラリが行うような単純なGETリクエストを送信することを期待すると思いますが、実際にはトークンを取得する追加の処理が行われます。

誤解によって期待される挙動と実際の挙動の間にギャップが生じ、バグが発生したり、デバッグプロセスが複雑で混乱を招く可能性があります。

## ✏️ リファクタリングしてみる

サービスで作成した関数には、ライブラリの関数名と区別できる明確な名前を使用することで、関数の挙動を予測可能にすることができます。

::: code-group

```typescript [httpService.ts]
// このサービスは`http`というライブラリを使っています
import { http as httpLibrary } from "@some-library/http";

// ライブラリの関数名と区別されるようにネーミングを変えました
export const httpService = {
async getWithAuth(url: string) {
const token = await fetchToken();

// トークンをヘッダーに追加するなど認証のロジックを追加します
return httpLibrary.get(url, {
headers: { Authorization: `Bearer ${token}` }
});
}
};
```

```typescript [fetchUser.ts]
// http.tsで定義したhttpを持ってくるコード
import { httpService } from "./httpService";

export async function fetchUser() {
// 関数名から、この関数が認証済みのリクエストを送ることが分かります。
return await httpService.getWithAuth("...");
}
```

:::

こうすることで、関数の名前を見たときに挙動を誤解する可能性を減らすことができます。
他の開発者がこの関数を使用する際に、サービスで定義された関数であることを認識し、正しく使用できるようになります。

また、`getWithAuth`という名前にすることで、この関数が認証済みのリクエストを送信することを明確に伝えることができます。
105 changes: 105 additions & 0 deletions ja/code/examples/item-edit-modal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Props Drillingを解消する

<div style="margin-top: 16px">
<Badge type="info" text="結合度" />
</div>

Props Drillingは親コンポーネントと子コンポーネントの間に結合度が生じていることを示す明確なサインです。もしDrillingされているpropsが変更された場合、そのpropsを参照しているすべてのコンポーネントを修正しなくてはなりません。

## 📝 コード例

次のコードは、ユーザーが`item`を選択する際に使用する`<ItemEditModal />`コンポーネントです。
ユーザーがキーワードを入力してアイテムのリストを検索し、探していたアイテムを選択すると`onConfirm`が呼び出されます。

ユーザーが入力したキーワードは`keyword`、選択可能なアイテムは`items`、推薦アイテムのリストは`recommendedItems`のpropsとして渡されます。

```tsx 2,9-10,12-13,29-32
function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) {
const [keyword, setKeyword] = useState("");

// 他のItemEditModalに関するロジック...

return (
<Modal open={open} onClose={onClose}>
<ItemEditBody
items={items}
keyword={keyword}
onKeywordChange={setKeyword}
recommendedItems={recommendedItems}
onConfirm={onConfirm}
onClose={onClose}
/>
{/* ... 他のItemEditModalコンポーネント ... */}
</Modal>
);
}

function ItemEditBody({
keyword,
onKeywordChange,
items,
recommendedItems,
onConfirm,
onClose
}) {
return (
<>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Input
value={keyword}
onChange={(e) => onKeywordChange(e.target.value)}
/>
<Button onClick={onClose}>閉じる</Button>
</div>
<ItemEditList
keyword={keyword}
items={items}
recommendedItems={recommendedItems}
onConfirm={onConfirm}
/>
</>
);
}

// ...
```

## 👃 コードの不吉な臭いを嗅いでみる

### 結合度

このコンポーネントは、親である`ItemEditModal`と子である`ItemEditBody``ItemEditList`などが同じ値である`recommendedItems``onConfirm``keyword`などをpropsとして共有しています。このため、親コンポーネントがpropsをそのまま子コンポーネントに渡す[Props Drilling](https://kentcdodds.com/blog/prop-drilling)が発生しています。

Props Drillingが発生すると、propsを不必要に参照するコンポーネントの数が増えます。
さらにpropsが変更されると、そのpropsを参照しているすべてのコンポーネントを修正する必要があります。

例えば、アイテムの推薦機能がなくなり、`recommendedItems`を削除する必要になった場合、関連するすべてのコンポーネントからこのpropsを削除しなければなりません。
コードの修正範囲が必要以上に広がり、結合度が高くなります。

## ✏️ リファクタリングしてみる

親コンポーネントが子コンポーネントにそのままpropを渡すProps Drillingを排除する必要があります。次のようにコンポジション(Composition)パターンを活用できます。

```tsx
function ItemEditModal({ open, items, recommendedItems, onConfirm, onClose }) {
const [keyword, setKeyword] = useState("");

return (
<Modal open={open} onClose={onClose}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Input
value={keyword}
onChange={(e) => onKeywordChange(e.target.value)}
/>
<Button onClick={onClose}>閉じる</Button>
</div>
<ItemEditList
keyword={keyword}
items={items}
recommendedItems={recommendedItems}
onConfirm={onConfirm}
/>
</Modal>
);
}
```
231 changes: 231 additions & 0 deletions ja/code/examples/login-start-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# 実装の詳細を抽象化する

<div style="margin-top: 16px">
<Badge type="info" text="可読性" />
</div>

一人の人がコードを読むときに同時に考慮できるコンテキストの数には限りがあります。そのため、コードを読む人がコードを簡単に理解できるように、不必要なコンテキストを抽象化する必要があります。

## 📝 コード例 1: LoginStartPage

次の`<LoginStartPage />`コンポーネントは、ユーザーがログインしているかを確認し、ログインしている場合はホームに移動させるロジックを持っています。

```tsx
function LoginStartPage() {
useCheckLogin({
onChecked: (status) => {
if (status === "LOGGED_IN") {
location.href = "/home";
}
}
});

/* ... ログインに関連するロジック ... */

return <>{/* ... ログインに関連するコンポーネント ... */}</>;
}
```

### 👃 コードの不吉な臭いを嗅いでみる

#### 可読性

例のコードでは、ログインが行われているかを確認し、ユーザーをホームに移動させるロジックが抽象化されずに露出しています。そのため、`useCheckLogin``onChecked、status``"LOGGED_IN"`といった変数や値をすべて読まなければ、コードの役割を理解することができません。

さらに、このコードの下には実際にログインに関連するコードが続いているため、読む人は`LoginStartPage`が何をしているのかを理解するために、一度に考慮しなければならないコンテキストが増えてしまいます。

### ✏️ リファクタリングしてみる

ユーザーがログインしているかどうかを確認し、移動するロジックを **HOC(Higher-Order Component)** またはWrapperコンポーネントに分離することで、コードを読む人が一度に知っておくべきコンテキストを減らすことができます。
これにより、コードの可読性を向上させることができます。

さらに、分離されたコンポーネント内のロジック同士の参照を防ぐことで、コード間の不必要な依存関係が生じて複雑になるのを防ぐことができます。

#### オプションA: Wrapperコンポーネントを使う

```tsx
function App() {
return (
<AuthGuard>
<LoginStartPage />
</AuthGuard>
);
}

function AuthGuard({ children }) {
const status = useCheckLoginStatus();

useEffect(() => {
if (status === "LOGGED_IN") {
location.href = "/home";
}
}, [status]);

return status !== "LOGGED_IN" ? children : null;
}

function LoginStartPage() {
/* ... ログインに関連するロジック ... */

return <>{/* ... ログインに関連するコンポーネント ... */}</>;
}
```

#### オプションB: HOC(Higher-Order Component)を使う

```tsx
function LoginStartPage() {
/* ... ログインに関連するロジック ... */

return <>{/* ... ログインに関連するコンポーネント ... */}</>;
}

export default withAuthGuard(LoginStartPage);

// HOC定義
function withAuthGuard(WrappedComponent) {
return function AuthGuard(props) {
const status = useCheckLoginStatus();

useEffect(() => {
if (status === "LOGGED_IN") {
location.href = "/home";
}
}, [status]);

return status !== "LOGGED_IN" ? <WrappedComponent {...props} /> : null;
};
}
```

## 📝 コード例 2: FriendInvitation

次の`<FriendInvitation />`コンポーネントは、クリックするとユーザーに同意を求め、ユーザーに招待を送るページコンポーネントです。

```tsx 6-27,33
function FriendInvitation() {
const { data } = useQuery(/* 省略.. */);

// このコンポーネントに必要な状態管理、イベントハンドラー、および非同期作業のロジックなど...

const handleClick = async () => {
const canInvite = await overlay.openAsync(({ isOpen, close }) => (
<ConfirmDialog
title={`${data.name}さんにシェアする`}
cancelButton={
<ConfirmDialog.CancelButton onClick={() => close(false)}>
閉じる
</ConfirmDialog.CancelButton>
}
confirmButton={
<ConfirmDialog.ConfirmButton onClick={() => close(true)}>
確認
</ConfirmDialog.ConfirmButton>
}
/* 省略 */
/>
));

if (canInvite) {
await sendPush();
}
};

// このコンポーネントに必要な状態管理、イベントハンドラー、および非同期作業のロジックなど...

return (
<>
<Button onClick={handleClick}>招待する</Button>
{/* UI用のJSXマークマップ... */}
</>
);
}
```

### 👃 コードの不吉な臭いを嗅いでみる

#### 可読性

可読性を保つためには、コードが一度に持つコンテキストは少ない方が良いです。1つのコンポーネントが多くの異なるコンテキストを持っていると、そのコンポーネントの役割を一目で理解するのが難しくなります。

`<FriendInvitation />`コンポーネントは、実際にユーザーに同意を得る際に使用する詳細なロジックまで1つのコンポーネントに含まれています。そのため、コードを読む際に追うべきコンテキストが多く、読みづらくなっています。

#### 凝集度

ユーザーに同意を得るロジックと、そのロジックを実行する実際の`<Button />`の間に隔たりがあるため、実際にどこでこのロジックが実行されているのかを確認するには、下の方にかなりスクロールをしないといけません。

そのため、頻繁に一緒に修正されるコードであるボタンとクリックハンドラーが、うまく一緒に修正されない可能性があります。

### ✏️ リファクタリングしてみる

ユーザーに同意を得るロジックとボタンを`<InviteButton />`コンポーネントとして抽象化しました。

```tsx
export function FriendInvitation() {
const { data } = useQuery(/* 省略.. */);

// このコンポーネントに必要な状態管理、イベントハンドラー、および非同期作業のロジックなど...

return (
<>
<InviteButton name={data.name} />
{/* UI用のJSXマークマップ... */}
</>
);
}

function InviteButton({ name }) {
return (
<Button
onClick={async () => {
const canInvite = await overlay.openAsync(({ isOpen, close }) => (
<ConfirmDialog
title={`${name}さんにシェアする`}
cancelButton={
<ConfirmDialog.CancelButton onClick={() => close(false)}>
閉じる
</ConfirmDialog.CancelButton>
}
confirmButton={
<ConfirmDialog.ConfirmButton onClick={() => close(true)}>
確認
</ConfirmDialog.ConfirmButton>
}
/* 省略 */
/>
));

if (canInvite) {
await sendPush();
}
}}
>
招待する
</Button>
);
}
```

`<InviteButton />`コンポーネントは、ユーザーを招待するロジックとUIだけを持っているため、一度に把握する内容が少なくなり、可読性が向上します。また、ボタンとクリック後の処理が近くにあるため、ロジックの流れも理解しやすくなっています。

## 🔍 もっと調べる: 抽象化

Tossの技術ブログの記事ではコードを文で比喩しています。[宣言的なコードを書く](https://toss.tech/article/frontend-declarative-code)

### 文での抽象化

「左に10歩歩け」という文があります。ここで、

- 「左」は「北を向いたときに90度回転した位置」を抽象化したもので、
- 「90度」は「1回の回転を360等分した角度の90倍だけ基準線に対して反時計回りに回ったこと」を抽象化したものであり、
- 「時計回り」の定義は「北半球で日時計の針が回る方向」を抽象化したものです。

同様に「10歩」や「歩け」といった言葉もより具体的に表現できます。抽象化せずにそのまま文を表すと、次のようになります。
> 北を向いたとき、1回の回転を360等分した角度の90倍だけ北半球で日時計の針が回る方向に回り、動物が陸上で足を使って移動する最も速い方法よりも遅く、身体をある地点から別の地点に移動させる行為を10回繰り返せ。
この文はそのまま読むと、どんな意味なのかを正確に理解するのが難しいです。

### コードでの抽象化

コードにおいても、実装の詳細を過度に明示すると、そのコードが果たす役割を正確に理解するのが難しくなります。
一度に6〜7個のコンテキストを考慮するのではなく、より小さな単位で抽象化することが必要です。。
50 changes: 50 additions & 0 deletions ja/code/examples/magic-number-cohesion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# マジックナンバーを排除する

<div style="margin-top: 16px">
<Badge type="info" text="凝集度" />
</div>

**マジックナンバー**(Magic Number)とは、正確な意味を説明せずにソースコードの中に直接数字の値を入れることを指します。

例えば、見つからないことを示すHTTPステータスコードとして`404`の値をそのまま使用することや、
1日を表す`86400`秒をそのまま使用することがこれに該当します。

## 📝 コード例

次のコードは、いいねボタンを押した時にいいねの数を新しく取得する関数です。

```typescript 3
async function onLikeClick() {
await postLike(url);
await delay(300);
await refetchPostLike();
}
```

## 👃 コードの不吉な臭いを嗅いでみる

### 凝集度

`300`という数字がアニメーションの完了を待つために使われている場合、再生するアニメーションを変更した際に、サービスが予期せず壊れるリスクがあります。十分な時間を置かずに、アニメーションが終了する前に次のロジックが始まってしまう可能性もあります。

また、修正が必要なコードの片方だけが変更されることから、凝集度が低いコードとも言えます。

::: info

この Hook は[可読性](./magic-number-readability.md)の観点からも考えることができます。

:::

## ✏️ リファクタリングしてみる

数字の`300`というコンテキストを正確に示すために、定数の`ANIMATION_DELAY_MS`として宣言することができます。

```typescript 1,5
const ANIMATION_DELAY_MS = 300;

async function onLikeClick() {
await postLike(url);
await delay(ANIMATION_DELAY_MS);
await refetchPostLike();
}
```
61 changes: 61 additions & 0 deletions ja/code/examples/magic-number-readability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# マジックナンバーに名前を付ける

<div style="margin-top: 16px">
<Badge type="info" text="可読性" />
</div>

**マジックナンバー**(Magic Number)とは、正確な意味を説明せずにソースコードの中に直接数字の値を入れることを指します。

例えば、見つからないことを示すHTTPステータスコードとして`404`の値をそのまま使用することや、
1日を表す`86400`秒をそのまま使用することがこれに該当します。

(Magic Number)

## 📝 コード例

次のコードは、いいねボタンを押した時にいいねの数を新しく取得する関数です。

```typescript 3
async function onLikeClick() {
await postLike(url);
await delay(300);
await refetchPostLike();
}
```

## 👃 コードの不吉な臭いを嗅いでみる

### 可読性

このコードは、`delay`関数に渡された`300`という値がどのようなコンテキストで使われているのかが不明です。
元のコードを書いた開発者でなければ、300msの待機がどのような目的で行われているのかは分かりません。

- アニメーションが完了するまで待機しているのか?
- いいねを反映させるのに時間がかかって待機しているのか?
- テストコードをうっかり削除し忘れただけなのか?

複数の開発者が1つのコードを共同で修正していく中で、その意図が正確に理解できないと、コードが意図しない方向に修正される可能性があります。

::: info

この Hook は[凝集度](./magic-number-cohesion.md)の観点からも考えることができます。

:::

### ✏️ リファクタリングしてみる

数字の`300`というコンテキストを正確に示すために、定数の`ANIMATION_DELAY_MS`として宣言することができます。

```typescript 1,5
const ANIMATION_DELAY_MS = 300;

async function onLikeClick() {
await postLike(url);
await delay(ANIMATION_DELAY_MS);
await refetchPostLike();
}
```

## 🔍 もっと調べてみる

マジックナンバーは凝集度の観点からも考えることができます。[マジックナンバーを排除する](./magic-number-cohesion.md)のドキュメントも参考にしてみてください。
72 changes: 72 additions & 0 deletions ja/code/examples/submit-button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 一緒に実行されないコードを分離する

<div style="margin-top: 16px">
<Badge type="info" text="可読性" />
</div>

同時に実行されないコードが1つの関数やコンポーネントにあると、挙動を一目で把握するのが難しくなります。
実装部分に多くの分岐が含まれていると、どのような役割を果たしているのか理解しづらくなります。

## 📝 コード例

次の`<SubmitButton />`コンポーネントは、ユーザーの権限に応じて異なる挙動をします。

- ユーザーの権限が閲覧専用(`"viewer"`)の場合、招待ボタンは無効化されており、アニメーションも再生されません。
- ユーザーが一般ユーザーの場合、招待ボタンを使用でき、アニメーションも再生されます。

```tsx
function SubmitButton() {
const isViewer = useRole() === "viewer";

useEffect(() => {
if (isViewer) {
return;
}
showButtonAnimation();
}, [isViewer]);

return isViewer ? (
<TextButton disabled>Submit</TextButton>
) : (
<Button type="submit">Submit</Button>
);
}
```

## 👃 コードの不吉な臭いを嗅いでみる

### 可読性

`<SubmitButton />`コンポーネントでは、ユーザーが持つ2つの権限状態を1つのコンポーネント内で同時に処理しています。そのため、コードを読む人が一度に考慮しなければならないコンテキストが多くなります。

例えば、次のコードで青色の部分はユーザーが閲覧専用権限(`'viewer'`)を持っているときに実行されるコードで、赤色の部分は一般ユーザーの場合に実行されるコードです。同時に実行されないコードが交差して現れるため、コードを理解するのが難しくなってしまっています。

![](../../../images/examples/submit-button.png)


## ✏️ リファクタリングしてみる

次のコードは、ユーザーが閲覧専用権限を持つ場合と一般ユーザーの場合を完全に分けて管理するようにしたものです。

```tsx
function SubmitButton() {
const isViewer = useRole() === "viewer";

return isViewer ? <ViewerSubmitButton /> : <AdminSubmitButton />;
}

function ViewerSubmitButton() {
return <TextButton disabled>Submit</TextButton>;
}

function AdminSubmitButton() {
useEffect(() => {
showAnimation();
}, []);

return <Button type="submit">Submit</Button>;
}
```

- `<SubmitButton />`のコードの至る所にあった分岐が1つにまとめられ、分岐が減りました。
- `<ViewerSubmitButton />``<AdminSubmitButton />`では1つの分岐のみを管理しているため、コードを読む人が一度に考慮しなければならないコンテキストが少なくなります。
40 changes: 40 additions & 0 deletions ja/code/examples/ternary-operator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 三項演算子をシンプルにする

<div style="margin-top: 16px">
<Badge type="info" text="可読性" />
</div>

三項演算子を複雑に使用すると、条件の構造が明確に見えなくなり、コードが読みづらくなることがあります。

## 📝 コード例

次のコードは、`A条件``B条件`に応じて、`BOTH``A`、または`NONE`のいずれかを`status`に指定するコードです。

```typescript
const status =
A条件 && B条件 ? "BOTH" : どちらも違う場合 ? "NONE" : A条件 ? "A" : undefined;
```

## 👃 コードの不吉な臭いを嗅いでみる

### 可読性

このコードは複数の三項演算子がネストされて使用されているため、正確にどの条件で値が計算されているのかを一目で把握するのが難しいです。

## ✏️ リファクタリングしてみる

次のように条件を`if文`で展開すると、より明確で簡潔に条件を示すことができます。

```typescript
const status = (() => {
if (A条件 && B条件) {
return "BOTH";
}

if (A条件) {
return "A条件";
}

return "NONE";
})();
```
57 changes: 57 additions & 0 deletions ja/code/examples/use-bottom-sheet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 重複コードを許容する

<div style="margin-top: 16px">
<Badge type="info" text="結合度" />
</div>

開発者として、複数のページやコンポーネントにわたる重複コードを1つのHookやコンポーネントに共通化することがよくあります。
重複コードを1つのコンポーネントやHookに共通化することで、良いコードの特徴の1つである凝集度を高め、共に修正が必要なコードを一度に修正することができます。

しかし、不要な結合度が生じることで、共通コンポーネントやHookを修正する際に影響を受けるコードの範囲が広がり、逆に修正が難しくなることもあります。

最初は似たような動作すると考えて共通化したコードが、後にページごとに異なる特異な仕様が生じて、次第に複雑化する可能性があります。
同時に、共通コードを修正するたびに、そのコードに依存するコードを一つ一つ適切にテストしなければならず、逆にコード修正が難しくなることもあります。

## 📝 コード例

次のように、点検情報を引数として受け取り、点検中であれば点検のボトムシートを開き、ユーザーが通知を受け取ることに同意した場合はそれをログに記録し、現在の画面を閉じるHookを見てみましょう。

```typescript
export const useOpenMaintenanceBottomSheet = () => {
const maintenanceBottomSheet = useMaintenanceBottomSheet();
const logger = useLogger();

return async (maintainingInfo: TelecomMaintenanceInfo) => {
logger.log("点検のボトムシートを開く");
const result = await maintenanceBottomSheet.open(maintainingInfo);
if (result) {
logger.log("点検のボトムシートの通知を受け取るをクリック");
}
closeView();
};
};
```

このコードは複数のページで繰り返し使用されたため、共通のHookとして分離されました。

## 👃 コードの不吉な臭いを嗅いでみる

### 結合度

このHookは複数のページで繰り返し見られるロジックであるため、共通化されました。しかし、将来生じる可能性のあるさまざまなコード変更を考慮する必要があります。

- もしページごとにロギングする値が異なる場合は?
- もしあるページでは点検ボトムシートを閉じても画面を閉じる必要がない場合は?
- ボトムシートで表示されるテキストや画像を異なるものにする必要がある場合は?

このHookはこうしたコード変更に柔軟に対応するために、複雑に引数を受け取る必要があるでしょう。
また、このHookの実装を修正するたびに、このHookを使用しているすべてのページが正常に動作するかどうかをテストしなければなりません。

## ✏️ リファクタリングしてみる

一見すると繰り返しのように見えるコードでも、重複コードを許可することが良い方向性である場合があります。

チームメンバーと積極的にコミュニケーションを取りながら、点検ボトムシートの動作を正確に理解する必要があります。
もしページでロギングする値が同じで、点検ボトムシートの動作や見た目が同じであり、今後もそうであるならば、共通化することでコードの凝集度を高めることができます。

しかし、ページごとに動作が異なる可能性がある場合は、共通化せずに重複コードを許可する方が良い選択肢となるでしょう。
96 changes: 96 additions & 0 deletions ja/code/examples/use-page-state-coupling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 責任を一つずつ管理する

<div style="margin-top: 16px">
<Badge type="info" text="結合度" />
</div>

クエリパラメータ、状態、APIの呼び出しなどのロジックを種類ごとに関数やコンポーネント、Hookに分けないでください。
一度に扱うコンテキストの種類が増えると、理解しづらく修正が難しいコードになってしまいます。

## 📝 コード例

次の`usePageState()`Hookはページ全体のURLクエリパラメータをすべて管理しています。

```typescript
import moment, { Moment } from "moment";
import { useMemo } from "react";
import {
ArrayParam,
DateParam,
NumberParam,
useQueryParams
} from "use-query-params";

const defaultDateFrom = moment().subtract(3, "month");
const defaultDateTo = moment();

export function usePageState() {
const [query, setQuery] = useQueryParams({
cardId: NumberParam,
statementId: NumberParam,
dateFrom: DateParam,
dateTo: DateParam,
statusList: ArrayParam
});

return useMemo(
() => ({
values: {
cardId: query.cardId ?? undefined,
statementId: query.statementId ?? undefined,
dateFrom:
query.dateFrom == null ? defaultDateFrom : moment(query.dateFrom),
dateTo: query.dateTo == null ? defaultDateTo : moment(query.dateTo),
statusList: query.statusList as StatementStatusType[] | undefined
},
controls: {
setCardId: (cardId: number) => setQuery({ cardId }, "replaceIn"),
setStatementId: (statementId: number) =>
setQuery({ statementId }, "replaceIn"),
setDateFrom: (date?: Moment) =>
setQuery({ dateFrom: date?.toDate() }, "replaceIn"),
setDateTo: (date?: Moment) =>
setQuery({ dateTo: date?.toDate() }, "replaceIn"),
setStatusList: (statusList?: StatementStatusType[]) =>
setQuery({ statusList }, "replaceIn")
}
}),
[query, setQuery]
);
}
```

## 👃 コードの不吉な臭いを嗅いでみる

### 結合度

このHookは、「このページに必要なすべてのクエリパラメータを管理する」という広範な責任を持っています。そのため、ページ内のコンポーネントや他のHookがこのHookに依存することになり、コード修正時に影響範囲が急激に広がる可能性があります。

時間が経つにつれて、このHook はメンテナンスがますます難しくなり、修正が困難なコードになってしまう可能性があります。

::: info

このHookは[可読性](./use-page-state-readability.md)の観点からも考えることができます。

:::

## ✏️ リファクタリングしてみる

次のコードのように、各クエリパラメータごとに別々のHookを作成することができます。

```typescript
import { useQueryParam } from "use-query-params";

export function useCardIdQueryParam() {
const [cardId, _setCardId] = useQueryParam("cardId", NumberParam);

const setCardId = useCallback((cardId: number) => {
_setCardId({ cardId }, "replaceIn");
}, []);

return [cardId ?? undefined, setCardId] as const;
}
```

Hookが担う責任を分離したため、修正による影響範囲を狭めることができます。
これにより、Hookを修正した際に予期しない影響が生じるのを防ぐことができます。
99 changes: 99 additions & 0 deletions ja/code/examples/use-page-state-readability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# ロジックの種類に応じて一体化している関数を分ける

<div style="margin-top: 16px">
<Badge type="info" text="可読性" />
</div>

クエリパラメータ、状態、APIの呼び出しなどのロジックを種類ごとに関数やコンポーネント、Hookに分けないでください。
一度に扱うコンテキストの種類が増えると、理解しづらく修正が難しいコードになってしまいます。

## 📝 コード例

次の`usePageState()`Hookはページ全体のURLクエリパラメータをすべて管理しています。

```typescript
import moment, { Moment } from "moment";
import { useMemo } from "react";
import {
ArrayParam,
DateParam,
NumberParam,
useQueryParams
} from "use-query-params";

const defaultDateFrom = moment().subtract(3, "month");
const defaultDateTo = moment();

export function usePageState() {
const [query, setQuery] = useQueryParams({
cardId: NumberParam,
statementId: NumberParam,
dateFrom: DateParam,
dateTo: DateParam,
statusList: ArrayParam
});

return useMemo(
() => ({
values: {
cardId: query.cardId ?? undefined,
statementId: query.statementId ?? undefined,
dateFrom:
query.dateFrom == null ? defaultDateFrom : moment(query.dateFrom),
dateTo: query.dateTo == null ? defaultDateTo : moment(query.dateTo),
statusList: query.statusList as StatementStatusType[] | undefined
},
controls: {
setCardId: (cardId: number) => setQuery({ cardId }, "replaceIn"),
setStatementId: (statementId: number) =>
setQuery({ statementId }, "replaceIn"),
setDateFrom: (date?: Moment) =>
setQuery({ dateFrom: date?.toDate() }, "replaceIn"),
setDateTo: (date?: Moment) =>
setQuery({ dateTo: date?.toDate() }, "replaceIn"),
setStatusList: (statusList?: StatementStatusType[]) =>
setQuery({ statusList }, "replaceIn")
}
}),
[query, setQuery]
);
}
```

## 👃 コードの不吉な臭いを嗅いでみる

### 可読性

このHookの責務が「ページで必要なすべてのクエリパラメータを管理すること」となると、責務が無限に増える可能性があります。新しいクエリパラメータを追加する際には、自然とこのHookがそれを管理することになるでしょう。その結果、Hookの担当範囲が広がり、コードが長くなってしまい、どのような役割を果たしているのか理解するのが難しくなります。

### パフォーマンス

このHookを使っているコンポーネントは、このHookが管理しているクエリパラメータが修正されたとしても、再レンダーが発生します。例えば、あるコンポーネントで`cardId`だけを参照しても、`dateFrom``dateTo`が変更されると再レンダーされます。
良いパフォーマンスのためには、特定の状態の値が更新された時、最低限な部分だけが再レンダーされるように設計するべきです。

::: info

このHookは[結合度](./use-page-state-coupling.md)の観点からも考えることができます。

:::

### ✏️ リファクタリングしてみる

次のように、各クエリパラメータごとに別々のHookを作成することができます。

```typescript
import { useQueryParam } from "use-query-params";

export function useCardIdQueryParam() {
const [cardId, _setCardId] = useQueryParam("cardId", NumberParam);

const setCardId = useCallback((cardId: number) => {
_setCardId({ cardId }, "replaceIn");
}, []);

return [cardId ?? undefined, setCardId] as const;
}
```

Hookが担う責任を分離したため、従来の`usePageState()`Hookよりも明確な名前を持っています。
また、Hookを修正した際に影響を及ぼす範囲を狭めることで、予期しない変更が発生するのを防ぐことができます。
180 changes: 180 additions & 0 deletions ja/code/examples/use-user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# 同じ種類の関数は返り値の型を揃える

<div style="margin-top: 16px">
<Badge type="info" text="予測可能性" />
</div>

API を叩くのと関連した Hook のように同じ種類の関数やHookがお互い違うタイプの返り値を持っていると、コードの一貫性が損なわれ、一緒に働くチームメンバーがコードを読むのが困難になります。

## 📝 コード例 1: useUser

次の`useUser``useServerTime`HookはすべてAPIを叩くのと関連したHookです。

しかし`useUser``@tanstack/react-query``Query`オブジェクトを返し、`useServerTime`はサーバー時間を持ってきてデータだけを返しています。

```typescript 9,18
import { useQuery } from '@tanstack/react-query';

function useUser() {
const query = useQuery({
queryKey: ['user'],
queryFn: () => fetchUser(),
});

return query;
}

function useServerTime() {
const query = useQuery({
queryKey: ['serverTime'],
queryFn: () => fetchServerTime(),
});

return query.data;
}
```

### 👃 コードの不吉な臭いを嗅いでみる

#### 予測可能性

サーバーのAPIを叩くHookの返り値のタイプがお互いに違うと、チームメンバーはこのようなHookを使うたびに返り値が何なのか確認しないといけません。`Query`オブジェクトを返すと、`data`を取り出す必要があり、データだけを返してあげればそのまま値を使えますよね。

同じように動くコードが一貫性を持って規則的でないとコードを読むのが難しくなります。

### ✏️ リファクタリングしてみる

次のようにサーバーのAPIを叩くHookは一貫性を持たせて`Query`オブジェクトを返してあげるようにすれば、チームメンバーがコードを予測できる可能性が高まります。

```typescript 9,18
import { useQuery } from '@tanstack/react-query';

function useUser() {
const query = useQuery({
queryKey: ['user'],
queryFn: () => fetchUser(),
});

return query;
}

function useServerTime() {
const query = useQuery({
queryKey: ['serverTime'],
queryFn: () => fetchServerTime(),
});

return query;
}
```

## 📝 コード例 2: checkIsValid

次の`checkIsNameValid``checkIsAgeValid`はすべての名前と年齢が正しいか検証する関数です。

```typescript
/** ユーザーネームは20字未満でないといけません。 */
function checkIsNameValid(name: string) {
const isValid = name.length > 0 && name.length < 20;

return isValid;
}

/** ユーザーは年齢が18歳以上99歳次の自然数でないといけません。 */
function checkIsAgeValid(age: number) {
if (!Number.isInteger(age)) {
return {
ok: false,
reason: "年齢は整数でないといけません。"
};
}

if (age < 18) {
return {
ok: false,
reason: "年齢は18歳以上でないといけません。"
};
}

if (age > 99) {
return {
ok: false,
reason: "年齢は99歳以下でないといけません。"
};
}

return { ok: true };
}
```

### 👃 コードの不吉な臭いを嗅いでみる

#### 予測可能性

検証関数の返り値が違うと、チームメンバーは関数を使うたびに返り値を確認する必要があり、コードリーディングが難しくなってしまいます。

特に[厳密なブールの比較](https://typescript-eslint.io/rules/strict-boolean-expressions/)のような機能を使わない場合、コードにパグが見つかる原因になっていまいます。

```typescript
// このコードは名前が規則を守っているか検証します。
if (checkIsNameValid(name)) {
// ...
}

// この関数はいつもオブジェクト { ok, ... }を返すので、
// `if`文内にあるコードはいつも信用できます
if (checkIsAgeValid(age)) {
// ...
}
```

### ✏️ リファクタリングしてみる

次のコードのように検証関数が一貫的に`{ ok, ... }`タイプのオブジェクトを返すようにすることができます。

```typescript
/** ユーザーの名前は20字未満でないといけません */
function checkIsNameValid(name: string) {
if (name.length === 0) {
return {
ok: false,
reason: "名前は空の値ではいけません。"
};
}

if (name.length > 20) {
return {
ok: false,
reason: "名前は20字まで入力できます。"
};
}

return { ok: true };
}

/** ユーザーは年齢が18歳以上、99歳次の自然数でなければなりません */
function checkIsAgeValid(age: number) {
if (!Number.isInteger(age)) {
return {
ok: false,
reason: "年齢は整数でないといけません。"
};
}

if (age < 18) {
return {
ok: false,
reason: "年齢は18歳以上でないといけません。"
};
}

if (age > 99) {
return {
ok: false,
reason: "年齢は99歳以下でないといけません。"
};
}

return { ok: true };
}
```
107 changes: 107 additions & 0 deletions ja/code/examples/user-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 視点の移動を減らす

<div style="margin-top: 16px">
<Badge type="info" text="可読性" />
</div>
コードを読む際に、コードを上下にスクロールしながら読んだり、色々なファイルや関数、変数を行き来しながら読むことを**視点移動**と言います。
視点が何度も移動すればするほど、コードを把握するのに時間がかかり、コンテキストを理解するのが困難になる可能性があります。

コードを上下に一つの関数やファイルで読めるように書くことで、読む人がコードの内容を瞬時に把握できるようになります。

## 📝 コード例

次のコードは、ユーザーの権限に応じて異なるボタンを表示します。

- ユーザーの権限が管理者(Admin)であれば、`Invite``View` ボタンを表示します。
- ユーザーの権限が閲覧専用(Viewer)であれば、`Invite`ボタンを無効化し、`View`ボタンを表示します。

```typescript
function Page() {
const user = useUser();
const policy = getPolicyByRole(user.role);

return (
<div>
<Button disabled={!policy.canInvite}>Invite</Button>
<Button disabled={!policy.canView}>View</Button>
</div>
);
}

function getPolicyByRole(role) {
const policy = POLICY_SET[role];

return {
canInvite: policy.includes("invite"),
canView: policy.includes("view")
};
}

const POLICY_SET = {
admin: ["invite", "view"],
viewer: ["view"]
};
```

## 👃 コードの不吉な臭いを嗅いでみる

### 可読性

このコードで`Invite`ボタンが無効化されている理由を理解するには、`policy.canInvite``getPolicyByRole(user.role)``POLICY_SET`の順でコードを行ったり来たりしながら読まないといけません。
この過程で3回の視点移動が起こるため、コードを読む人がコンテキストを維持しながら読むのが困難になります。

`POLICY_SET`のような抽象化を使って権限によってボタンの状態を管理することは権限の仕様が複雑な場合には有効ですが、今のような簡単な場合には、逆に読む人がコードを理解しにくくしてしまいます。

## ✏️ リファクタリングしてみる

### A. 条件を展開してそのまま表す

権限条件の仕様をそのままコードで表す方法です。このようにすれば`Invite`ボタンが無効化されている場合でもコードですぐに確認することができます。
コードを上から下へ読むだけで一目で権限を扱うロジックについて理解することができるようになります。

```typescript
function Page() {
const user = useUser();

switch (user.role) {
case "admin":
return (
<div>
<Button disabled={false}>Invite</Button>
<Button disabled={false}>View</Button>
</div>
);
case "viewer":
return (
<div>
<Button disabled={true}>Invite</Button>
<Button disabled={false}>View</Button>
</div>
);
default:
return null;
}
}
```

### B. 条件を一目で把握できるオブジェクトにする

権限を扱うロジックをコンポーネントの中でオブジェクトで管理して、何度も視点移動をしなくても一目で条件を把握できるように修正できます。
`canInvite``canView`の条件を`Page`コンポーネントを見れば確認できようになりました。

```tsx
function Page() {
const user = useUser();
const policy = {
admin: { canInvite: true, canRead: true },
viewer: { canInvite: false, canView: true }
}[user.role];

return (
<div>
<Button disabled={!policy.canInvite}>Invite</Button>
<Button disabled={!policy.canView}>View</Button>
</div>
);
}
```
75 changes: 75 additions & 0 deletions ja/code/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
comments: false
---

# 変更しやすいコード

良いフロントエンドコードは **変更しやすい** コードです。
新しい仕様を実装する際に、既存のコードを修正してリリースのしやすいコードが良いコードだと言えます。
コードが変更しやすいかどうかは4つの基準で判断できます。

## 1. 可読性

**可読性**(Readability)は、コードがどれだけ読みやすいかを指します。コードを変更しやすくするためには、まずそのコードがどのように動くのかを理解する必要があります。読みやすいコードは、読む人が考えることを最小限に抑え、上から下へと自然に流れるように実装されています。

### 可読性を向上させる戦略

- **コンテキストを減らす**
- [一緒に実行されないコードを分離する](./examples/submit-button.md)
- [実装の詳細を抽象化する](./examples/login-start-page.md)
- [ロジックの種類に応じて一体化している関数を分ける](./examples/use-page-state-readability.md)
- **名前付け**
- [複雑な条件に名前を付ける](./examples/condition-name.md)
- [マジックナンバーに名前を付ける](./examples/magic-number-readability.md)
- **上から下へ読めるようにする**
- [視点の移動を減らす](./examples/user-policy.md)
- [三項演算子をシンプルにする](./examples/ternary-operator.md)

## 2. 予測可能性

**予測可能性**(Predictability)とは、一緒に働くチームメンバーたちが関数やコンポーネントの挙動をどれだけ予測できるかを指します。
予測可能性が高いコードは、一貫したルールに従い、関数やコンポーネントの名前、パラメータ、返り値だけを見てもどのような挙動をするのかが分かります。

### 予測可能性を高める戦略

- [名前が被らないように管理する](./examples/http.md)
- [同じ種類の関数は返り値の型を統一する](./examples/use-user.md)
- [隠れたロジックを露呈させる](./examples/hidden-logic.md)

## 3. 凝集度

**凝集度**(Cohesion)とは、修正されるべきコードが常に一緒に修正されるかどうかを指します。凝集度が高いコードは、コードの一部を修正しても意図せず他の部分でエラーが発生しません。これは、一緒に修正されるべき部分が必ず一緒に修正されるようにコードが実装されているためです。

::: info 可読性と凝集度は相反することがあります

一般的に、凝集度を高めるためには変数や関数を抽象化するなど、可読性を低下させる決断をする必要があります。
一緒に修正されないとエラーが発生する可能性がある場合には、凝集度を優先してコードを共通化・抽象化してください。
リスクが高くない場合には、可読性を優先してコードの重複を許可してください。

:::

### 凝集度を高める戦略

- [一緒に修正されるファイルは同じディレクトリに置く](./examples/code-directory.md)
- [マジックナンバーを排除する](./examples/magic-number-cohesion.md)
- [フォームの凝集度について考える](./examples/form-fields.md)

## 4. 結合度

**結合度**(Coupling)とは、コードを修正したときの影響範囲を指します。コードを修正した際に影響範囲が小さく、変更に伴う影響を予測できるコードが、修正しやすいコードと言えます。

### 結合度を下げる戦略

- [責任を一つずつ管理する](./examples/use-page-state-coupling.md)
- [重複コードを許容する](./examples/use-bottom-sheet.md)
- [Props Drilling を解消する](./examples/item-edit-modal.md)

## コード品質を多角的に見る

残念ながら、この 4つの基準をすべて同時に満たすことは難しいです。

例えば、関数や変数を常に一緒に修正できるように共通化や抽象化を行うと、凝集度が高まります。しかし、その結果としてコードが一度抽象化されるため、可読性が低下します。

重複コードを許容することで、コードの影響範囲を減らし、結合度を下げることができますが、片方を修正した時にもう片方を誤って修正し忘れてしまう可能性があるため、凝集度が低下します。

フロントエンド開発者は、現在直面している状況を考慮し、よく考えた上で、長期的にコードの修正が容易になるにはどれを優先すべきかを検討する必要があります。
34 changes: 34 additions & 0 deletions ja/code/start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
comments: false
---

# はじめる

`Frontend Fundamentals`は、フロントエンドコードのベストプラクティスを提供します。フロントエンド開発者としてコードの品質を向上させたい時に、方向を見つけるためのコンパスのように活用してみてください。

良いコードに関する[4 つの原則](./index.md)とともに、具体的な例と解決策を提示します。

## こんな時に活用してみてください

- 🦨 コードについて悩んでいるけれど **論理的に説明できない開発者**
- 👀 **悪いコードを素早く見つけて**改善する方法を学びたい開発者
- 🤓 コードレビューなどで他の誰かに指摘してもらうことで、自分のコードがどうなっているのかを**客観的に認識**したい開発者。
- 👥 **チームメンバーと一緒に** 共通のコーディングスタイルとコード品質の基準を決めたい開発者

## 制作者

- [milooy](https://github.com/milooy)
- [donghyeon](https://github.com/kimbangg)
- [chkim116](https://github.com/chkim116)
- [inseong.you](https://github.com/inseong.you)
- [raon0211](https://github.com/raon0211)
- [bigsaigon333](https://github.com/bigsaigon333)
- [jho2301](https://github.com/jho2301)
- [KimChunsick](https://github.com/KimChunsick)
- [jennybehan](https://github.com/jennybehan)

## ドキュメント貢献者

- [andy0414](https://github.com/andy0414)
- [pumpkiinbell](https://github.com/pumpkiinbell)
- [jennybehan](https://github.com/jennybehan)
28 changes: 28 additions & 0 deletions ja/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home

hero:
name: "Frontend Fundamentals"
tagline: "簡単に変更できるフロントエンドコードのガイドライン"
image:
src: /images/ff-symbol-gradient.png
alt: Frontend Fundamentals symbol
actions:
- text: 良いコードについて学ぶ
link: /ja/code/
- theme: alt
text: コミュニケーション
link: /ja/code/community

features:
- icon: 🤓
title: コードレビューのスキルを向上させる
details: コードが簡単に変更できるかどうかを判断するための原則を探ります。
- icon: 🤝
title: より良いコードレビューを行う
details: 様々なコード改善のケースを積極的に探ります。
- icon: 📝
title: 自分のコードが気になりますか?
details: GitHubのディスカッションで他の開発者とコミュニケーションを取りましょう。
---